import binascii
import codecs
import json
import os
import queue
import re
import socket
import tempfile
import threading
import time
from collections import namedtuple

import loguru
import stackprinter


ThreadPortConf = namedtuple("ThreadPortConf", ["modbus_addr", "addr", "port", "queue"])

logger = loguru.logger
logger.add(
    os.path.join(tempfile.gettempdir(), "fenliu.log"),
    enqueue=True,
    rotation="10 minutes",
    retention="7 days",
    compression="lzma",
)
# lzma 12288


def get_conf(conf_file_path):
    with open(conf_file_path, "r") as fp:
        conf_dict = json.load(fp)
        main_conf = conf_dict["main_conf"]
        thread_confs = []
        for conf in conf_dict["thread_confs"]:
            thread_confs.append(
                ThreadPortConf(
                    modbus_addr=str2Bytes(conf["modbus_addr"]),
                    addr=conf.get("server_addr"),
                    port=conf.get("server_port"),
                    queue=queue.Queue(),
                )
            )
        return main_conf, thread_confs


def str2Bytes(st: str):
    clear_st = re.sub("[^0-9a-fA-F]", "", st)
    if len(clear_st) % 2 == 1:
        clear_st = clear_st[:-1] + "0" + clear_st[-1]
    result_bytes = binascii.a2b_hex(clear_st)
    return result_bytes


def bytesPuttyPrint(b: bytes):
    """直观打印数据包"""
    if len(b) == 0:
        return "None Pack"
    st = codecs.encode(b, "hex").decode("utf-8").upper()
    it = iter(st)
    result_str = " ".join(a + b for a, b in zip(it, it))
    return result_str


def reconect_client(sub_conf, logger=loguru.logger):
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    connect_result = False
    while not connect_result:
        try:
            client.connect((sub_conf.addr, sub_conf.port))
        except Exception as e:
            logger.info("连接失败... 60秒后重试 {}:{} | {}".format(sub_conf.addr, sub_conf.port, repr(e)))
            time.sleep(60)
        else:
            logger.info("连接成功 {}".format("{}:{}".format(sub_conf.addr, sub_conf.port)))
            return client


def thread_task(sub_conf, logger=loguru.logger):
    """执行具体的分发逻辑"""
    info = "装置地址 {} --> 服务器 {}:{}".format(bytesPuttyPrint(sub_conf.modbus_addr), sub_conf.addr, sub_conf.port)
    client = reconect_client(sub_conf, logger)
    last_data = None
    data = None
    while True:
        while sub_conf.queue.qsize() > 100:
            logger.info("队列过长 | {} | 挑选与上次不同报文发送".format(sub_conf.queue.qsize()))
            data = sub_conf.queue.get()
            if data != last_data:
                break
        else:
            data = sub_conf.queue.get()
        try:
            start = time.perf_counter()
            client.send(data)
            logger.debug(
                "开始向 {}:{} 发送完毕 长度 {} 耗时 {:.6f}mS".format(
                    sub_conf.addr, sub_conf.port, len(data), 1000 * (time.perf_counter() - start)
                )
            )
        except Exception:
            exception_format = stackprinter.format()
            logger.error("发送异常 10秒后重连 {}\n{}".format(info, exception_format))
            time.sleep(10)
            client = reconect_client(sub_conf, logger)
            continue
        else:
            last_data = data
            queue_size = sub_conf.queue.qsize()
            if queue_size > 0:
                logger.info("剩余队列长度 {}".format(queue_size))


def pack_deal(sock, client_address, sub_confs, logger=loguru.logger):
    """分发数据包"""
    logger.info("入站来源 {}".format(client_address))
    while True:
        try:
            data = sock.recv(1024)
        except Exception:  # ConnectionResetError
            exception_format = stackprinter.format()
            logger.error("分发线程异常 | {}".format(exception_format))
            sock.close()
            break
        else:
            if data is None or len(data) == 0:
                break
            send_flag = False
            for sub_conf in sub_confs:
                if data.startswith(sub_conf.modbus_addr):
                    sub_conf.queue.put(data)
                    logger.info(
                        "分发数据 {} | 长度 {} 到 {}:{}".format(bytesPuttyPrint(data), len(data), sub_conf.addr, sub_conf.port)
                    )
                    send_flag = True
            if send_flag is False:
                data_show = bytesPuttyPrint(data)
                logger.info("数据包无人认领 | {}".format(data_show))


def main_server(server_addr, server_port, sub_confs, logger=loguru.logger):
    """主进程用于接收数据包 并执行分发"""
    logger.info("入站监听地址 {}:{}".format(server_addr, server_port))
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((server_addr, server_port))
    server.listen(5)
    for sub_conf in sub_confs:
        t = threading.Thread(target=thread_task, args=(sub_conf, logger))
        t.start()
    while True:
        sock, client_address = server.accept()
        logger.info("开始处理入站数据")
        t = threading.Thread(target=pack_deal, args=(sock, client_address, sub_confs, logger))
        t.start()


if __name__ == "__main__":
    try:
        main_conf, thread_confs = get_conf(r"./fenliu_conf.json")
        main_server(main_conf["addr"], main_conf["port"], thread_confs, loguru.logger)
    except Exception:
        logger.error("运行异常 程序退出\n{}".format(stackprinter.format()))
        input("按回车退出")
